package com.googlecode.dgwt.rebind.feature;

import java.io.File;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.TreeLogger.Type;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.rebind.AbstractSourceCreator;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;
import com.googlecode.dgwt.client.dojo.Feature;
import com.googlecode.dgwt.client.dojo.Feature.Inherits;
import com.googlecode.dgwt.client.dojo.Feature.MainClass;
import com.googlecode.dgwt.client.dojo.Feature.SuperClass;
import com.googlecode.dgwt.rebind.base.DoubleSourceWriter;

public class FeatureSourceCreator extends AbstractSourceCreator {

    
    public static final String FEATURE_CLASSNAME = Feature.class.getName().replace('$', '.');
    
    public static final int CONTENT_MAIN = 0;
    public static final int CONTENT_INHERITED = 1;
    public static final int CONTENT_SUPER_STUB = 2;
    
    
    private JClassType targetClass;
    private SourceWriter writer;
    private ClassInfo targetInfo;
    
    protected final TreeLogger logger;
    protected final TypeOracle typeOracle;
    protected final GeneratorContext context;
    protected final JClassType featureClass;
    
    protected String packageName;
    protected List<ClassInfo> implList;

    protected Map<String, ClassInfo> allClasses = new LinkedHashMap<String, ClassInfo>();
    protected Map<String, InterfaceInfo> allInterfaces = new HashMap<String, InterfaceInfo>();
    
    
    
    
    public FeatureSourceCreator(GeneratorContext context, TreeLogger logger, TypeOracle typeOracle) {
        this.logger = logger;
        this.context = context;
        this.typeOracle = typeOracle;
        this.featureClass = typeOracle.findType(FEATURE_CLASSNAME);
    }

    
    public String emitClass(JClassType aTargetClass) throws UnableToCompleteException {
        
        targetClass = aTargetClass;
        JClassType interf = targetClass;

        if (interf.isInterface() != null) {
            Inherits inherits = interf.getAnnotation(Inherits.class);
            if (inherits == null) {
                throw error(logger, "Missing @Inherits on Feature \"" + interf.getQualifiedSourceName() + "\"");
            }
            
            JClassType classe = typeOracle.findType(inherits.value().getName().replace('$', '.'));
            
            if (!interf.isAssignableFrom(classe)) {
                throw error(logger, "Inherited class \"" + classe.getQualifiedSourceName() + 
                        "\" should implements \"" + interf.getQualifiedSourceName() + "\"");
            }
            
            targetClass = classe;
        }
        
        targetInfo = new ClassInfo(targetClass, interf);
        targetInfo.setNewClassName(targetClass.getName().replace('.', '_') + "_DGWT_Feature_Impl");
        targetInfo.setGetterName("this");
        packageName = targetClass.getPackage().getName();
        implList = new ArrayList<ClassInfo>(findAllInherits());

        //defineSuperClassHierarchy(); //***
        //findAllMethodAnnotation(); //*** ODO modificar para por estes metodos dentro de needsRebind()

        if (!targetClass.isAbstract() && targetInfo.superClasses.isEmpty()) {
            return aTargetClass == targetClass ? null : targetClass.getQualifiedSourceName();
        }

        this.rebindTargetClass();
        
        PrintWriter pw = context.tryCreate(logger, packageName, targetInfo.getNewClassName());

        if (pw != null) {
            ClassSourceFileComposerFactory factory = new ClassSourceFileComposerFactory(packageName, targetInfo.getNewClassName());
            
            if (targetClass.isInterface() != null) {
                factory.addImplementedInterface(targetClass.getQualifiedSourceName());
            } else {
                factory.setSuperclass(targetClass.getQualifiedSourceName());
            }
            
            
            writer = factory.createSourceWriter(context, pw);
            
            // FIXME  remover em produção
            try {
                File file = new File("C:/Workspace/WELL/Temp/" + packageName.replace('.', '/') + "/");
                file.mkdirs();
                writer = new DoubleSourceWriter(writer, factory.createSourceWriter(new PrintWriter(new File(file, targetInfo.getNewClassName() + ".java"))));
            } catch (Exception e) {
                logger.log(Type.WARN, e.getMessage());
            }
                        
            classPrologue();
            subClassContent(targetInfo, CONTENT_MAIN);
            classEpilog();
            
            writer.commit(logger);
        }

        return packageName + "." + targetInfo.getNewClassName();
    }

    
    private void classPrologue() {
        writer.println();
        writer.println("private Object impl;");
        writer.println();
        
        MainLoop:
        
        for (ClassInfo classInfo : targetInfo.superClasses) {
            
            for (MethodInfo methodInfo : targetInfo.methods) {
                if (methodInfo.injection.injectedClass == classInfo) {
                    classInfo.setGetterName(methodInfo.injection.method.getName());
                    continue MainLoop;
                }
            }

            generateInjection(classInfo, false, "private", classInfo.getGetterName());
        }
    }

    
    private void generateInjection(ClassInfo classInfo, boolean override, String modifier, String methodName) {
        
        if (override) {
            writer.println("@Override");
        }
        
        final String fullTargetName = packageName + "." + targetInfo.getNewClassName();
        final String fullTargetAssignture = "L" + fullTargetName.replace('.', '/') + ";";
        
        final String fullClassName = fullTargetName + "." + classInfo.getNewClassName();
        final String fullClassAssignture = "L" + fullTargetName.replace('.', '/') + "$" + classInfo.getNewClassName() + ";";
        
        writer.print(modifier);
        writer.print(" native ");
        writer.print(classInfo.typeIntf.getParameterizedQualifiedSourceName());
        writer.print(" ");
        writer.print(methodName);
        writer.println("() /*-{");
        writer.indent();
        {
            writer.println("var impl, child, stub;");
            
            
            writer.print("if (!(impl = this.@");
            writer.print(fullTargetName);
            writer.println("::impl))");
            writer.indent();
            {
                writer.print("impl = this.@");
                writer.print(fullTargetName);
                writer.println("::impl = {};");
            }
            writer.outdent();
         
            
            writer.print("if (!(child = impl.");
            writer.print(classInfo.getGetterName());
            writer.println("))");
            writer.indent();
            {
                writer.print("child = impl.");
                writer.print(classInfo.getGetterName());
                writer.print(" = @");
                writer.print(fullClassName);
                writer.print("::new(");
                writer.print(fullTargetAssignture);
                writer.println(")(this);");
            }
            writer.outdent();
            
            if (classInfo.hasStub()) {
                writer.print("if (!(stub = child.@");
                writer.print(fullClassName);
                writer.println("::stub))");
                writer.indent();
                {
                    writer.print("return child.@");
                    writer.print(fullClassName);
                    writer.print("::stub");
                    writer.print(" = @");
                    writer.print(fullClassName);
                    writer.print(".Stub");
                    writer.print("::new(");
                    writer.print(fullClassAssignture);
                    writer.println(")(child);");
                }
                writer.outdent();

                writer.println("return stub;");
            } else {
                writer.println("return child;");
            }
        }
        writer.outdent();
        writer.println("}-*/;");
        writer.println();        
    }
    
    
    private void classEpilog() throws UnableToCompleteException {
        for (ClassInfo classInfo : implList) {
            
            if (!classInfo.needsRebind()) {
                continue;
            }
            
            writer.print("public class ");
            writer.print(classInfo.getNewClassName());
            writer.print(" extends ");
            writer.print(classInfo.type.getQualifiedSourceName());
            writer.println(" {");
            writer.println();
            writer.indent();
            {
                if (classInfo.hasStub()) {                
                    writer.println("private Object stub;");
                    writer.println();
                    subClassContent(classInfo, CONTENT_INHERITED);
                } else {
                    subClassContent(classInfo, CONTENT_MAIN);
                }
                
                if (classInfo.hasStub()) {                
                    writer.print("public class Stub ");
                    boolean first=true;
                    
                    for (InterfaceInfo intf : classInfo.interfaces) {
                        if (first) {
                            first = false;
                            writer.print("implements ");
                        } else {
                            writer.print(", ");
                        }
                        writer.print(intf.type.getParameterizedQualifiedSourceName());
                    }
                    
                    writer.println(" {");
                    writer.println();
                    writer.indent();
                    {
                        subClassContent(classInfo, CONTENT_SUPER_STUB);    
                    }
                    writer.outdent();
                    writer.println("}");
                }
            }
            writer.outdent();
            writer.println("}");
            writer.println();
        }
    }


    private void subClassContent(ClassInfo classInfo, int contentType) throws UnableToCompleteException {
        
        for (MethodInfo info : classInfo.methods) {
         
            if (contentType == CONTENT_SUPER_STUB) {
                if (info.interfaces.isEmpty()) {
                    continue;
                }
            }
            else if (info.inherits == classInfo
                    && info.injection.annotationSuperClass == null) {
                continue;
            }
            
            String returnType = info.method.getReturnType().getQualifiedSourceName();
            
            if (returnType.indexOf(" extends ") > 0) {
                returnType = "<" + returnType + "> " + info.method.getReturnType().getParameterizedQualifiedSourceName();
            }
            
            if (info.injection.annotationSuperClass != null && classInfo == targetInfo) {
                generateInjection(info.injection.injectedClass, true, "public", info.method.getName());
                continue;
            }
            
            writer.println("@Override");
            writer.print("public ");
            writer.print(returnType);
            writer.print(" ");
            writer.print(info.method.getName());
            writer.print("(");
            
            JParameter[] params = info.method.getParameters();
            
            for (int i=0; i<params.length; i++) {
                if (i != 0) {
                    writer.print(", ");
                }
                
                writer.print(params[i].getType().getParameterizedQualifiedSourceName());
                writer.print(" arg" + i);
            }
            
            writer.println("){");
            writer.indent();
            {
                if (info.injection.injectedClass == null) {
                    if (!returnType.equals("void")) {
                        writer.print("return (");
                        writer.print(info.method.getReturnType().getParameterizedQualifiedSourceName());
                        writer.print(") ");
                    }

                    if (contentType == CONTENT_SUPER_STUB) {
                        if (info.inherits == classInfo) {
                            writer.print(classInfo.getNewClassName());
                            writer.print(".super");
                        } else {
                            writer.print(targetInfo.getNewClassName());
                            writer.print(".this");
                        }
                    } 
                    else if (contentType == CONTENT_INHERITED) {
                        writer.print(targetInfo.getNewClassName());
                        writer.print(".this");
                    } 
                    else {
                        writer.print(getQualifiedFieldName(info.inherits));
                    }
                    
                    writer.print(".");
                    writer.print(info.method.getName());
                    writer.print("(");
    
                    for (int i=0; i<params.length; i++) {
                        if (i != 0) {
                            writer.print(", ");
                        }                    
                        writer.print("arg" + i);
                    }
    
                    writer.println(");");
                } else {
                
                    if (info.injection.annotationMainClass != null) {
                        writer.print("return (");
                        writer.print(info.method.getReturnType().getParameterizedQualifiedSourceName());
                        writer.print(") ");
                        writer.print(targetInfo.getNewClassName());
                        writer.println(".this;");
                    }
                    else
                    if (info.injection.annotationSuperClass != null) {
                        writer.print("return (");
                        writer.print(info.method.getReturnType().getParameterizedQualifiedSourceName());
                        writer.print(") ");
                        writer.print(getQualifiedFieldName(info.injection.injectedClass));
                        writer.println(";");
                    }
                }
            }
            writer.outdent();
            writer.println("}");
            writer.println();
        }
    }


    private void rebindTargetClass() throws UnableToCompleteException {
        defineSuperClassHierarchy();
        findAllMethodAnnotation();
        defineMethodsHierarchy();
        defineInterfaceHierarchy(targetInfo);
    }

    
    /**
     * define hierarquia de interfaces
     */
    private void defineInterfaceHierarchy(ClassInfo classInfo) {
        classInfo.inherits = new HashMap<InterfaceInfo, ClassInfo>(classInfo.interfaces.size());
        
        for (ClassInfo superClass : classInfo.superClasses) {
            
            for (InterfaceInfo intf : classInfo.interfaces) {
            
                if (superClass.interfaces.contains(intf)) {
                    
                    classInfo.setInherit(intf, superClass);
                    defineInterfaceHierarchy(superClass);
                }
            }
        }
    }


    /**
     * define hierarquia de implementações
     * @throws UnableToCompleteException 
     */
    private void defineSuperClassHierarchy() throws UnableToCompleteException {
        
        defineSuperClassHierarchyAux(targetInfo);
        
        int id=0;
        
        LoopHierarquia:
            
            for (int j=0; j<implList.size();) {
                ClassInfo parent = implList.get(j);
                
                for (int i=0, len=implList.size(); i<len; i++) {                    
                    if (i != j) {
                        ClassInfo item = implList.get(i);
                        
                        if (parent.type.isAssignableFrom(item.type)) {
                            if (i > j) {
                                implList.remove(j);
                            } 
                            else {
                                implList.set(j, item);
                                implList.remove(i);
                            }
                            item.setGeneratedNames(targetInfo, ++id);
                            parent.setNamesFrom(item);
                            continue LoopHierarquia;  
                        }
                    }
                }
                
                parent.setGeneratedNames(targetInfo, ++id);
                j++;
            }
        
    }


    private void defineSuperClassHierarchyAux(ClassInfo classInfo) throws UnableToCompleteException {
        
        for (ClassInfo childInfo : classInfo.superClasses) {
            implList.add(childInfo);
            defineSuperClassHierarchyAux(childInfo);
        }        
    }


    /**
     * Define hierarquia de metodos
     * 
     * @throws UnableToCompleteException 
     */
    private void defineMethodsHierarchy() throws UnableToCompleteException {

        for (int j=-1, len=implList.size(); j<len; j++) {
            ClassInfo classInfo;
            
            if (j == -1) {
                classInfo = targetInfo;
            } else {
                classInfo = implList.get(j);
            }
            
            LoopMethods:
                
            for (MethodInfo methodInfo : classInfo.methods) {
                
                if (!methodInfo.isInherited) {
                    methodInfo.inherits = classInfo;
                    continue;
                }
                
                if (methodInfo.method.isAbstract() 
                        && methodInfo.method.isDefaultAccess() 
                        && !packageName.equals(classInfo.type.getPackage().getName())){
                    
                    throw error(logger, "Unable to implement method with default access \"" + methodInfo.method.getName()+"\"");
                }
                
                for (int i=j+1; i<len; i++) {
                    ClassInfo childInfo = implList.get(i);
    
                    for (MethodInfo childMethod : childInfo.methods) {
                        
                        if (methodInfo.inherits == null && methodInfo.key.equals(childMethod.key)) {
                            methodInfo.inherits = childInfo;
                            continue LoopMethods;
                        }                    
                    }
                }
                
                if (classInfo == targetInfo 
                        && methodInfo.inherits == null 
                        && methodInfo.injection.injectedClass == null) {
                    throw error(logger, "No implementation found for abstract method \"" + methodInfo.method.getEnclosingType().getQualifiedSourceName() + "." + methodInfo.method.getName()+"\"");
                }
            }
        }
    }
    
    
    private void findAllMethodAnnotation() throws UnableToCompleteException {
        
        for (int j=-1, len=implList.size(); j<len; j++) {
            ClassInfo classInfo;
            
            if (j == -1) {
                classInfo = targetInfo;
            } else {
                classInfo = implList.get(j);
            }
            
            for (MethodInfo info : classInfo.methods){
                JMethod method = info.method;
                
                SuperClass annotSuperClass = info.injection.annotationSuperClass;
                MainClass annotMainClass = info.injection.annotationMainClass;
                
                if (annotSuperClass == null && annotMainClass == null) {
                    continue;
                }
                
                if (annotSuperClass != null && annotMainClass != null) {
                    throw error(logger, "Cann't inject @SuperClass and @MainClass in the same method \"" + classInfo.type.getQualifiedSourceName() + "." + method.getName() + "\"");
                }
                
                if (!method.isPublic() && !method.isProtected()) {
                    if (!method.isDefaultAccess() || !packageName.equals(classInfo.type.getPackage().getName())){
                        
                        throw error(logger, "Unable to inject @SuperClass in method with private or default access \"" + classInfo.type.getQualifiedSourceName() + "." + method.getName() + "\"");
                    }
                }
                
                FieldInjection injection = info.injection;
                
                if (annotMainClass == null) {
                    JClassType type = typeOracle.findType(annotSuperClass.value().getName().replace('$', '.'));
                    
                    if (!method.isAbstract()) {
                        throw error(logger, "Unable to inject @SuperClass. Method should be abstract \"" + classInfo.type.getQualifiedSourceName() + "." + method.getName() + "\"");
                    }

                    if (type.getQualifiedSourceName().equals(FEATURE_CLASSNAME)
                            || type == typeOracle.getJavaLangObject()) {
                        
                        type = method.getReturnType().isClassOrInterface();
                    }

                    if (type != null) {                        
                        for (int i=j+1; i<len; i++) {
                            ClassInfo superInfo = implList.get(i);
                            
                            if (type.isAssignableFrom(superInfo.type)) {
                                injection.injectedClass = superInfo;
                                break;
                            }
                        }
                    }

                } else {
                    JClassType type = method.getReturnType().isClassOrInterface();
                    
                    if (!type.isAssignableFrom(targetClass)) {
                        throw error(logger, "Unable to inject @MainClass. Method with incompatible return type \"" + 
                                classInfo.type.getQualifiedSourceName() + "." + method.getName() + "\". It should be assignable from \"" + type.getQualifiedSourceName() + "\"");
                    }
                    
                    injection.injectedClass = targetInfo;
                }
                
                if (injection.injectedClass != null) {
                    classInfo.injections.add(injection);
                }
                else {
                    throw error(logger, "Unable to find inherited type for injection \"" + classInfo.type.getQualifiedSourceName() + "." + method.getName() + "\"");
                }
            }
        }
    }


    ///------- findAllInherits
    
    
    
    private int findAllInherits() throws UnableToCompleteException {        
        allClasses.put(targetClass.getQualifiedSourceName(), targetInfo);
        findAllInheritsAux(targetInfo, allClasses);
        return allClasses.size();
    }
    
    
    private void findAllInheritsAux(ClassInfo mainInfo, Map<String, ClassInfo> implMap) throws UnableToCompleteException {
        
        List<ClassInfo> classes = parseClasse(mainInfo);
        mainInfo.superClasses = new ArrayList<ClassInfo>(classes.size());
        
        for (int i=classes.size()-1; i>=0; i--) {
            JClassType classType = classes.get(i).type;            
            String name = classType.getQualifiedSourceName();
            
            if (implMap.containsKey(name)) {
                continue;
            }            
            if (!featureClass.isAssignableFrom(classType)) {
                throw error(logger, name + " should be instance of " + FEATURE_CLASSNAME);
            }
            
            ClassInfo info = classes.get(i);
            mainInfo.superClasses.add(0, info);
            implMap.put(name, info);
            findAllInheritsAux(info, implMap);            
        }
    }


    
    ///------- getMethodsAndInterfaces
    
    
    
    private List<ClassInfo> parseClasse(ClassInfo info) throws UnableToCompleteException {
        Map<String, MethodInfo> methods = new HashMap<String, MethodInfo>();
        Map<String, InterfaceInfo> interfaces = new HashMap<String, InterfaceInfo>();
        
        List<ClassInfo> inheritedClasses = getMethodsOfSuperCasses(info.type, methods, interfaces);
        
        info.methods = new ArrayList<MethodInfo>(methods.values());
        info.interfaces = new ArrayList<InterfaceInfo>(interfaces.values());
                
        return inheritedClasses;
    }

    
    private List<ClassInfo> getMethodsOfSuperCasses(JClassType type,
                                         Map<String, MethodInfo> m,
                                         Map<String, InterfaceInfo> interfaces) throws UnableToCompleteException {
        
        List<ClassInfo> inheritedClasses = new ArrayList<ClassInfo>();
        
        do {            
            for (JClassType subClass : type.getNestedTypes()) {
                MainClass mainClass = subClass.getAnnotation(MainClass.class);
                
                if (mainClass != null) {
                    inheritedClasses.add(new ClassInfo(subClass, subClass));
                }
            }
            
            for (JMethod method : type.getMethods()) {
                String key = uniqueMethodKey(method);
                MethodInfo info = m.get(key);
                
                if (info == null) {
                    info = new MethodInfo(method);
                    info.key = key;
                    info.isInherited = method.isAbstract();
                    info.interfaces = new ArrayList<InterfaceInfo>();
                    m.put(key, info);
                }
                
                try {
                    info.injection.addAnnotation(method.getAnnotation(SuperClass.class));
                    info.injection.addAnnotation(method.getAnnotation(MainClass.class));
                } catch (Exception e) {
                    throw error(logger, e.getMessage());
                }                
            }

            for (JClassType intf : type.getImplementedInterfaces()) {
                String name = intf.getQualifiedSourceName();
                InterfaceInfo info = allInterfaces.get(name);
                
                if (info == null) {
                    info = new InterfaceInfo(intf);
                    allInterfaces.put(name, info);
                }
                
                interfaces.put(name, info);
                getMethodsOfInterfaces(info, m, inheritedClasses, interfaces);
            }
            
            type = type.getSuperclass();
            
        } while(!type.getQualifiedSourceName().equals(FEATURE_CLASSNAME) 
                    && type != typeOracle.getJavaLangObject());
        
        return inheritedClasses;
    }

    private void getMethodsOfInterfaces(InterfaceInfo interf,
                                        Map<String, MethodInfo> m,
                                        List<ClassInfo> inheritedClasses,
                                        Map<String, InterfaceInfo> interfaces) throws UnableToCompleteException {


        Inherits inherits = interf.type.getAnnotation(Inherits.class);
        if (inherits != null) {
            JClassType classe = typeOracle.findType(inherits.value().getName().replace('$', '.'));
            
            if (!interf.type.isAssignableFrom(classe)) {
                throw error(logger, "Inherited class \"" + classe.getQualifiedSourceName() + 
                        "\" should implements \"" + interf.type.getQualifiedSourceName() + "\"");
            }
            
            inheritedClasses.add(new ClassInfo(classe, interf.type));
        }

        
        JMethod[] methods = interf.type.getMethods();

        for (JMethod method : methods) {
            String key = uniqueMethodKey(method);
            MethodInfo info = m.get(key);

            if (info == null) {
                info = new MethodInfo(method);
                info.key = key;
                info.isInherited = true;
                info.interfaces = new ArrayList<InterfaceInfo>();
                m.put(key, info);
            }

            try {
                info.injection.addAnnotation(method.getAnnotation(SuperClass.class));
                info.injection.addAnnotation(method.getAnnotation(MainClass.class));
            } catch (Exception e) {
                throw error(logger, e.getMessage());
            }                

            info.interfaces.add(interf);
        }

        JClassType[] supers = interf.type.getImplementedInterfaces();

        for (JClassType intf : supers) {
            String name = intf.getQualifiedSourceName();
            InterfaceInfo info = allInterfaces.get(name);

            if (info == null) {
                info = new InterfaceInfo(intf);
                allInterfaces.put(name, info);
            }

            interfaces.put(name, info);
            getMethodsOfInterfaces(info, m, inheritedClasses, interfaces);
        }
    }


    ///------- UTILS
    
    

    private String getQualifiedFieldName(ClassInfo info) {
        
        if (info == targetInfo) {
            return targetInfo.getNewClassName() +  "." + targetInfo.getGetterName();
        }
        
        return targetInfo.getNewClassName() +  ".this." + info.getGetterName() + "()";
    }


    protected static String uniqueMethodKey(JMethod method) {
        String name = method.getName();
        name += "(";
        JParameter[] m = method.getParameters();
        for (int i = 0; i < m.length; i++) {
            name += m[i].getType() + " ";
        }
        name += ")";
        return name;
    }


    // FIXME remover
    //@SuppressWarnings("unused")
    protected void fastLog(Object s) {
        logger.log(Type.WARN, ""+s);
    }


    public JClassType getTargetClass() {
        return targetClass;
    }


    public ClassInfo getTargetInfo() {
        return targetInfo;
    }


}
